import { ServerConnectAction } from '@/server/action'
import type { ServerConnect } from '@/server/connect'
import type { FileStructureDiffResult } from '@/utils/fileStructure'
import { getFileHash } from '@/utils/hash'
import config from '@/config'
import type { ActionsNext } from '@/utils/action'

export class FileReceiveAction extends ServerConnectAction {
  constructor(connect: ServerConnect) {
    super('fileReceive', connect)

    this.addAction('generateReceiveList', this.generateReceiveList_handler, true)
    this.addAction('fileReceive', this.fileReceive_handler)
    this.addAction('fileReceiveComplete', this.fileReceiveComplete_handler)

    this.actions.addBranchChain('generateReceiveList', 'fileReceive', 'fileReceiveComplete')
  }

  fileReceiveList: { path: string; hash: string }[] = []

  generateReceiveList_handler(next: ActionsNext) {
    const fileStructureDiffResult =
      this.connect.store.get<FileStructureDiffResult>('fileStructureDiffResult')

    for (const key in fileStructureDiffResult.sended) {
      this.fileReceiveList.push({
        path: key,
        hash: fileStructureDiffResult.sended[key]
      })
    }

    next()
  }

  receivedHash: string[] = []

  fileReceive_handler(next: ActionsNext) {
    const project = this.connect.project
    if (!project) return this.fail('project not init')

    this.logger.info('start receive file')

    const off = this.connect.packetOn(async (p) => {
      try {
        const hash = p.header.hash as string
        const chunkTotal = p.header.chunkTotal as number
        const count = p.header.count as number

        if (!this.isInReceiveList(hash as string)) {
          return await this.sendFatalError('nonexistent hash')
        }

        if (!p.body) {
          return await this.sendError('not data')
        }

        if (count === 1 && project.tempExist(hash)) {
          await project.remoteTemp(hash)
        }

        await project.appendTemp(hash, p.body).catch(async (err: Error) => {
          await this.sendError(String(err))
          return Promise.reject(err)
        })

        if (count >= chunkTotal) {
          const bool = await this.validateTempFile(hash)
          if (!bool) {
            // 文件校验hash失败，删除文件，并通知客户端重新发送
            await project.remoteTemp(hash)
            this.logger.error('file hash not match', hash)
            return await this.connect.sendError('file hash not match', {
              validateHashFailed: true
            })
          }

          this.receivedHash.push(hash)

          await this.connect.sendOk()
          this.logger.info('receive file success', this.receivedHash.length, hash)
          if (this.isReceivedComplete()) {
            off()
            next()
          }
        } else {
          await this.connect.sendOk()
        }
      } catch (error) {
        this.logger.error(error)
        this.sendError(String(error))
      }
    })
  }

  fileReceiveComplete_handler(next: ActionsNext) {
    this.logger.info('receive file complete')
    next()
  }

  isReceivedComplete() {
    if (this.receivedHash.length !== this.fileReceiveList.length) return false
    return this.fileReceiveList.every((item) => this.receivedHash.includes(item.hash))
  }

  async validateTempFile(hash: string) {
    const project = this.connect.project
    if (!project || !project.tempExist(hash)) return false
    try {
      this.connect.startIntervalSendPong(1000)
      const file = project.joinTempPath(hash)
      const fileHash = await getFileHash(file, {
        algorithm: config.cache.hashMapAlgorithm
      })
      return fileHash === hash
    } catch (error) {
      return false
    } finally {
      this.connect.stopIntervalSendPong()
    }
  }

  sendError(err: string) {
    return this.connect.sendError(err)
  }

  sendFatalError(err: string) {
    return this.connect.sendFatalError(err)
  }

  // 是否存在于接收列表
  isInReceiveList(hash: string) {
    return this.fileReceiveList.some((item) => item.hash === hash)
  }
}
